
package w83b.xml;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;

import org.apache.xpath.XPathAPI;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import w83b.xml.exception.W83bXmlErrorCodes;
import w83b.xml.exception.W83bXmlException;



/** Representa un documento xml. */

public final class W83bXmlDocument implements java.io.Serializable{

	private static final long serialVersionUID = 1L;
   private Document domDoc;

   /** Construye un objeto de la clase. */

   public W83bXmlDocument() {

      domDoc = null;
   }

   /** Construye un objeto de la clase. */

   public W83bXmlDocument(Document document) {

      domDoc = document;
   }

   /**
    * Crea el documento a partir de texto xml.
    * 
    * @param text
    *           Texto fuente.
    * @throws W83bXmlException
    *            si se produce algún error.
    */

   public void createFromStringText(String text) throws W83bXmlException {
      ByteArrayInputStream src;

      try {
         src = new ByteArrayInputStream(text.getBytes());
         domDoc = parse(src);
      }
      catch (Exception exc) {
         throw new W83bXmlException(W83bXmlErrorCodes.EC_LOAD_XML, exc);
      }

   }


/**
    * Crea el documento a partir de texto xml.
    * 
    * @param text
    *           Texto fuente.
    * @throws W83bXmlException
    *            si se produce algún error.
    */

   public void createFromUtf8StringText(String text) throws W83bXmlException {
      ByteArrayInputStream src;

      try {
         src = new ByteArrayInputStream(text.getBytes("UTF8"));
         domDoc = parse(src);
      }
      catch (Exception exc) {
         throw new W83bXmlException(W83bXmlErrorCodes.EC_LOAD_XML, exc);
      }

   }

   /**
    * Crea el documento a partir de texto xml codificado en UTF-8.
    * 
    * @param text
    *           Texto fuente.
    * @throws W83bXmlException
    *            si se produce algún error.
    */

   public void createFromUtf8Text(byte[] text) throws W83bXmlException {
      ByteArrayInputStream src;

      try {
         src = new ByteArrayInputStream(text);
         domDoc = parse(src);
      }
      catch (Exception exc) {
         throw new W83bXmlException(W83bXmlErrorCodes.EC_LOAD_XML, exc);
      }

   }

   /**
    * Crea el documento a partir del contenido xml de un fichero de recursos.
    * 
    * @param pathname
    *           Ruta de acceso y nombre del fichero fuente.
    * @throws W83bXmlException
    *            si se produce algún error.
    */

   public void createFromFile(String pathname)
            throws W83bXmlException, IOException {
      InputStream inputStream = null;
      try {
         inputStream = getClass().getResourceAsStream(pathname);
         domDoc = parse(inputStream);
      }
      catch (Exception exc) {
         throw new W83bXmlException(W83bXmlErrorCodes.EC_LOAD_XML, exc);
      }
      finally {
         if (inputStream != null)
         {
        	 inputStream.close();
         }
      }
   }

   /**
    * Crea el documento a partir del contenido xml de un fichero del sistema de
    * archivos.
    * 
    * @param pathname
    *           Ruta de acceso y nombre del fichero fuente.
    * @throws W83bXmlException
    *            si se produce algún error.
    */

   public void createFromSystemFile(String pathname)
            throws W83bXmlException, IOException {

      try {
         File src = new File(pathname);
         FileInputStream fis = new FileInputStream (src);
         domDoc = parse(fis);
      }
      catch (Exception exc) {
         throw new W83bXmlException(W83bXmlErrorCodes.EC_LOAD_XML, exc);
      }
   }

   /**
    * Devuelve el texto xml del documento.
    * 
    * @param omitXmlHdr
    *           Especifica si omitir la cabecera xml.
    * @return El texto mencionado.
    * @throws W83bXmlException
    *            si se produce algún error.
    */

   public String getStringText(boolean omitXmlHdr) throws W83bXmlException {

      return W83bXmlTransformer.transformXmlDocumentToXmlStringText(this,
               omitXmlHdr);
   }

   /**
    * Devuelve el texto xml del documento codificado en UTF-8.
    * 
    * @param omitXmlHdr
    *           Especifica si omitir la cabecera xml.
    * @return El texto mencionado.
    * @throws W83bXmlException
    *            si se produce algún error.
    */

   public byte[] getUtf8Text(boolean omitXmlHdr) throws W83bXmlException {

      return W83bXmlTransformer.transformXmlDocumentToXmlUtf8Text(this,
               omitXmlHdr);
   }

   /**
    * Escribe el documento en un fichero.
    * 
    * @param pathname
    *           Ruta de acceso y nombre del fichero.
    * @throws W83bXmlException
    *            si se produce algún error.
    */

   public void writeToFile(String pathname) throws W83bXmlException {

      W83bXmlTransformer.transformXmlDocumentToXmlFile(this, pathname);
   }

   /**
    * Devuelve el elemento raíz del documento.
    * 
    * @return El elemento mencionado.
    */

   public W83bXmlElement getRootElement() {

      W83bXmlElement elem = null;
      Element domElem;

      domElem = domDoc.getDocumentElement();
      elem = new W83bXmlElement(domElem);

      return elem;

   }

   /**
    * Devuelve el documento Dom subyacente.
    * 
    * @return El documento mencionado.
    */

   public Document getDomDocument() {

      return domDoc;
   }

   /**
    * Método que obtiene el valor de un elemento a través de una expresión
    * XPath.
    * 
    * @param xPathExpression
    *           Expresión que obtiene el valor deseado.
    * @return El valor del elemento.
    * @throws W83bXmlException
    *            si se produce algún error.
    */

   public String getValueOfElement(String xPathExpression)
            throws W83bXmlException {

      String value = null;
     // Node node;
      W83bXmlElement element;
      
      element = getXmlElement(xPathExpression);
      if (element == null)
      {
    	  throw new W83bXmlException(W83bXmlErrorCodes.EC_INVALID_ELEMENT_NAME);
      }
      
      value = element.getValue();
      
      return value;
   }
   
   /**
    * Método que obtiene el nombre del elemento padre a través de una expresión
    * XPath que apunta a uno de sus hijos.
    * 
    * @param xPathExpression
    *            Expresión que obtiene el valor deseado.
    * @return El nombre del elemento.
    * @throws W83bXmlException
    *             si se produce algún error.
    */

   public String getParentElementName(String xPathExpression)
   throws W83bXmlException {

      String value = null;
      Node node;

      try {
         node = XPathAPI.selectSingleNode((Node)getDomDocument(),
               xPathExpression);
         value = node.getParentNode().getNodeName().toString().trim();
         if (value.equals("")) {//NOPMD
            throw new W83bXmlException(
            		W83bXmlErrorCodes.EC_INVALID_ELEMENT_NAME);
         }

         return value;
      } 
      catch(TransformerException exc) {
         throw new W83bXmlException(W83bXmlErrorCodes.EC_IRRETRIEVABLE_VALUE, 
               exc);
      }
   }

   /**
    * Método que obtiene un nodo a través de una expresión XPath.
    * 
    * @param xpathExpression
    *           Expresión que obtiene el valor deseado.
    * @return El valor del nodo elemento. Si no existe se devuelve nulo
    * @throws W83bXmlException
    */

   public W83bXmlElement getXmlElement(String xpathExpression)
            throws W83bXmlException {

     // String value = null;
      Node node;
      W83bXmlElement element = null;

      try {
         node = XPathAPI.selectSingleNode(getDomDocument(), xpathExpression);
         if (node != null) {
        	 element = new W83bXmlElement((Element)node);
         }
         
         return element;

      }
      catch (TransformerException exc) {
         throw new W83bXmlException(W83bXmlErrorCodes.EC_IRRETRIEVABLE_VALUE,
                  exc);
      }

   }

   /**
    * Método que obtiene un conjunto de nodos a través de una expresión XPath.
    * 
    * @param xpathExpression
    *           Expresión que obtiene el valor deseado.
    * @return El valor de los nodos elemento. Si no existe se devuelve nulo
    * @throws W83bXmlException
    */

   public W83bXmlElements getXmlElements(String xpathExpression)
            throws W83bXmlException {

      NodeList nodeList;
      int counter;
      W83bXmlElements elements = null;
      W83bXmlElement[] elem;
     // W83bXmlElement element;

      try {
         nodeList = XPathAPI.selectNodeList(getDomDocument(), xpathExpression);

         elem = new W83bXmlElement[nodeList.getLength()];
         for (counter = 0; counter < nodeList.getLength(); counter++) {
            elem[counter] = new W83bXmlElement((Element)nodeList.item(counter));//NOPMD
         }
         elements = new W83bXmlElements(elem);
         
         return elements;
      }
      catch (TransformerException exc) {
         throw new W83bXmlException(W83bXmlErrorCodes.EC_IRRETRIEVABLE_VALUE,
                  exc);
      }

   }

   /*
    * NO FUNCIONA BIEN: public void insertCommentNodeBefore(String
    * commentToInsert, String referenceNodeName) throws W83bXmlException { Node
    * comment; Node referenceNode; NodeList nodeList;
    * 
    * try { comment = (Node)domDoc.createComment(commentToInsert); referenceNode =
    * XPathAPI.selectSingleNode((Node)getDomDocument(), "//" +
    * referenceNodeName); comment = domDoc.insertBefore(comment, referenceNode); }
    * catch(TransformerException te) { throw new
    * W83bXmlException(W83bXmlErrorCodes.EC_INVALID_ELEMENT_NAME, te); } }
    */

   /**
    * Método que obtiene una colección de elementos a partir de su nombre.
    * 
    * @param name
    *           Nombre de los elementos a recuperar.
    * @return Colección de elementos. Si no existe se devuelve nulo
    * @throws W83bXmlException
    */
   public W83bXmlElements getElementsByTagName(String name)
            throws W83bXmlException {

      NodeList nodeList = null;
      W83bXmlElements elements = null;
      W83bXmlElement[] elem;
      //W83bXmlElement element;

      try {
         nodeList = getDomDocument().getElementsByTagName(name);
         if (nodeList == null) {
            throw new W83bXmlException(W83bXmlErrorCodes.EC_INVALID_ELEMENT_NAME);
         }
         else {
            elem = new W83bXmlElement[nodeList.getLength()];
            for (int i = 0; i < nodeList.getLength(); i++) {
               elem[i] = new W83bXmlElement((Element)nodeList.item(i));//NOPMD
            }
            elements = new W83bXmlElements(elem);
         }
         return elements;
      }
      catch (Exception exc) {
         throw new W83bXmlException(W83bXmlErrorCodes.EC_TRANSFORM_EXCEPTION,
                  exc);
      }
   }

   /**
    * Selecciona el nodo del documento XML, el cual es dato miembro de la
    * presente clase, que determina una expresión XPath y devuelve en un objeto
    * de tipo Map el contenido de dicho nodo.
    * 
    * @param xPathExpression
    *           Expresión que obtiene el nodo deseado.
    * @return El conjunto de pares nombre de elemento - valor correspondiente a
    *         dicho nodo.
    * @throws W83bXmlException
    *            si se produce algún error.
    */
   public Map getNodeValuesById(String xPathExpression) throws W83bXmlException {

      Map nodeMap;
      Node actualNode;
      NodeList nodeList = null;

      try {
         nodeMap = new HashMap();

         nodeList = XPathAPI.selectNodeList(domDoc, xPathExpression);

         if (nodeList == null) {
            throw new W83bXmlException(W83bXmlErrorCodes.EC_INVALID_ELEMENT_NAME);
         }
         else {
            int nodeCounter;
            if (nodeList.getLength() == 1) {
               actualNode = nodeList.item(0);
               nodeList = actualNode.getChildNodes();

               for (nodeCounter = 0; nodeCounter < nodeList.getLength(); nodeCounter++) {
                  actualNode = nodeList.item(nodeCounter);
                  setNodeValuesIntoMap(actualNode, nodeMap);
               }
            }
            else
               if (nodeList.getLength() > 1) {
                  for (nodeCounter = 0; nodeCounter < nodeList.getLength(); nodeCounter++) {
                     NodeList actualNodeList;

                     actualNodeList = nodeList.item(nodeCounter)
                              .getChildNodes();
                     for (nodeCounter = 0; nodeCounter < actualNodeList.getLength(); nodeCounter++) {
                        actualNode = actualNodeList.item(nodeCounter);
                        setNodeValuesIntoMap(actualNode, nodeMap);
                     }
                  }
               }
         }
         return nodeMap;
      }
      catch (Exception exc) {
         throw new W83bXmlException(W83bXmlErrorCodes.EC_TRANSFORM_EXCEPTION,
                  exc);
      }
   }

   /*
    * Método recursivo que permite introducir los pares nombre de elemento -
    * valor en el objeto Map correspondiente.
    */
   private void setNodeValuesIntoMap(Node node, Map nodes) {

      if (node.getNodeType() == Node.TEXT_NODE) {
         if (!node.getNodeValue().trim().equals("")) {
            // Caso Base 1:
            nodes.put(node.getParentNode().getNodeName().toString().trim(),
                     node.getNodeValue().toString().trim());
         }
      }
      else
         if ((node.getNodeType() == Node.ELEMENT_NODE) && ((node.getFirstChild() != null))) {
            if ((node.getFirstChild().getNodeType() == Node.TEXT_NODE) && !node.getFirstChild()
                     .getNodeValue()
                     .trim()
                     .equals("")) {
               // Caso Base 2:
               nodes.put(node.getNodeName().toString().trim(),
                        node.getFirstChild().getNodeValue().toString().trim());
            }
            else
               if (node.getFirstChild().getNextSibling().getNodeType() == Node.ELEMENT_NODE) {
                  // Caso Recursivo:
                  NodeList childNodeList;
                  Node actualChildNode;
                  Map childMap;

                  childMap = new HashMap();
                  childNodeList = node.getChildNodes();

                  int childCounter;
                  for (childCounter = 0; childCounter < childNodeList.getLength(); childCounter++) {
                     actualChildNode = childNodeList.item(childCounter);
                     setNodeValuesIntoMap(actualChildNode, childMap);
                  }
                  nodes.put(node.getNodeName().toString().trim(), childMap);
               }
         }
   }

   private synchronized static Document parse (InputStream is) throws ParserConfigurationException, FactoryConfigurationError, SAXException, IOException {
       if (db == null)
       {
    	   db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
       }

       return db.parse(is);
   }
  
   private static DocumentBuilder db;
}